/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2007 Ben Motmans <ben.motmans@gmail.com>
 * Copyright (C) 2008 Philippe Durand <draekz@gmail.com>
 * Copyright (C) 2007-2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 * 
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers;
using System.Web;

using Anculus.Core;

using Galaxium.Core;
using Galaxium.Gui;

namespace Galaxium.Protocol.Msn
{
	public enum XFRRequestState { Incomplete, Requested, Completed };
	
	public class SBConnection : CommandConnection, IMsnP2PBridge
	{
		public event EventHandler<ContactEventArgs> ContactJoined;
		public event EventHandler<ContactEventArgs> ContactLeft;
		public event EventHandler<ContactEventArgs> InviteContactFailed;
		
		public event EventHandler BridgeClosed;
		public event EventHandler BridgeReady;
		
		string _auth;
		List<MsnContact> _contacts = new List<MsnContact> ();
		string _id;
		bool _localRequest = false;
		uint _authTimeout;
		Dictionary<MsnContact, uint> _inviteTimeouts = new Dictionary<MsnContact, uint> ();
		
		internal bool _authenticated = false;
		internal XFRRequestState _requestState = XFRRequestState.Incomplete;
		
		internal MsnContact[] _inviteContacts = new MsnContact[0];
		
		public XFRRequestState RequestState
		{
			get { return _requestState; }
			set { _requestState = value; }
		}
		
		public List<MsnContact> Contacts
		{
			get { return _contacts; }
		}
		
		public string ID
		{
			get { return _id; }
		}
		
		// The maximum amount of P2P data that can be sent per MSG via the switchboard
		public uint MaxDataSize
		{
			get { return 1150; }
		}
		
		// Return true if the connection is ready to send P2P data
		public bool Ready
		{
			get
			{
				return CanSend && (_contacts.Count == 1) && (_outQueue.Count == 0);
			}
		}
		
		public override bool CanSend
		{
			get { return base.CanSend && _authenticated && (Contacts.Count > 0); }
		}
		
		internal SBConnection (MsnSession session, bool priority, params MsnContact[] contacts)
			: base (session, new MsnSBConnectionInfo (session.Connection.Connection.ConnectionInfo.HostName, 1, session.Connection.Connection.ConnectionInfo.UseHTTP, string.Empty), MsnConnectionType.Switchboard)
		{
			_id = Guid.NewGuid ().ToString ();
			_localRequest = true;
			_inviteContacts = contacts;
			
			_requestState = XFRRequestState.Incomplete;
			
			Session.RequestSwitchboard (this, priority);
		}
		
		internal SBConnection (MsnSession session, MsnSBConnectionInfo info, string auth, string id)
			: base (session, info, MsnConnectionType.Switchboard)
		{
			_auth = auth;
			_id = id;
			
			_requestState = XFRRequestState.Completed;
			
			Connect ();
		}
		
		public void Connect (MsnSBConnectionInfo info, string auth, string id)
		{
			Connection.ConnectionInfo = info;
			
			_auth = auth;
			_id = id;
			
			Connect ();
		}
		
		internal void OnXFRResponseReceived (IMsnCommand cmd)
		{
			Log.Debug ("XFR response received, applying to switchboard connection...");
			
			_localRequest = true;
			Session._lastXfrCompleted = DateTime.Now;
			
			if (cmd is XFRCommand)
			{
				_requestState = XFRRequestState.Completed;
				
				XFRCommand xfr = cmd as XFRCommand;
				
				_auth = xfr.SwitchboardAuthentication;
				Connection.ConnectionInfo = new MsnSBConnectionInfo (xfr.HostName, xfr.Port, Connection.ConnectionInfo.UseHTTP, _auth);
				
				Connect ();
			}
			else
				Log.Warn ("Error requesting switchboard\n{0}", cmd);
		}
		
		protected override void OnAfterConnect (object sender, ConnectionEventArgs args)
		{
			base.OnAfterConnect (sender, args);
			
			Log.Debug ("SB {0} Connected (Initiated {1})", _id, _localRequest ? "Locally" : "Remotely");
			
			_authTimeout = TimerUtility.RequestCallback (AuthTimeout, 30000);
			
			if (_localRequest)
				Send (new SBUSRCommand (Session, _auth), true);
			else
				Send (new ANSCommand (Session, _auth, _id), true);
		}
		
		protected override void OnClosed (object sender, ConnectionEventArgs args)
		{
			_requestState = XFRRequestState.Incomplete;
			_authenticated = false;
			
			OnBridgeClosed ();

			while (_contacts.Count > 0)
			{
				MsnContact contact = _contacts[0];
				
				_contacts.Remove (contact);
				OnContactLeft (new ContactEventArgs (contact));
			}
			
			base.OnClosed (sender, args);
		}
		
		protected override void OnErrorOccurred (object sender, ConnectionErrorEventArgs args)
		{
			_requestState = XFRRequestState.Incomplete;
			_authenticated = false;
			
			OnBridgeClosed ();
			base.OnErrorOccurred (sender, args);
		}
		
#region Command Handlers
#pragma warning disable 169
		[CommandHandler]
		private void OnErrorReceived (ErrorCommand msg)
		{
			Log.Error ("Error {0} Occurred", msg.ErrorCode);
			
			if (!_authenticated)
			{
				// We got an error before we've finished authenticating
				// kill the connection...
				
				Disconnect ();
				return;
			}
		}
		
		[CommandHandler]
		void OnIROReceived (IROCommand cmd)
		{
			MsnContact contact = cmd.Contact;
			contact.ClientIdentifier = cmd.ClientIdentifier;
			
			_contacts.Add (contact);
			
			if (ContactJoined != null)
				ContactJoined (this, new ContactEventArgs (cmd.Contact));
			
			ProcessOutQueue ();
		}
		
		[CommandHandler]
		void OnJOIReceived (JOICommand cmd)
		{
			MsnContact contact = cmd.Contact;
			contact.ClientIdentifier = cmd.ClientIdentifier;
			
			if (_inviteTimeouts.ContainsKey (contact))
			{
				TimerUtility.RemoveCallback (_inviteTimeouts[contact]);
				_inviteTimeouts.Remove (contact);
			}
			
			_contacts.Add (contact);
			
			OnContactJoined (new ContactEventArgs (cmd.Contact));
			
			if (_contacts.Count > 1)
				OnBridgeClosed ();
			
			ProcessOutQueue ();
		}
		
		[CommandHandler]
		void OnUSRReceived (SBUSRCommand cmd)
		{
			foreach (MsnContact contact in _inviteContacts)
				Invite (contact, true);
			
			_inviteContacts = new MsnContact[0];
			
			_authenticated = true;

			if (_authTimeout != 0)
			{
				TimerUtility.RemoveCallback (_authTimeout);
				_authTimeout = 0;
			}
			
			ProcessOutQueue ();
		}
		
		[CommandHandler]
		void OnANSReceived (ANSCommand cmd)
		{
			_authenticated = true;
			
			if (_authTimeout != 0)
			{
				TimerUtility.RemoveCallback (_authTimeout);
				_authTimeout = 0;
			}
			
			ProcessOutQueue ();
		}
		
		[CommandHandler]
		void OnBYEReceived (BYECommand cmd)
		{
			if (_contacts.Contains (cmd.Contact))
			{
				_contacts.Remove (cmd.Contact);
				
				OnContactLeft (new ContactEventArgs (cmd.Contact));
				
				if (_contacts.Count == 0)
					Disconnect ();
			}
			else
				Log.Warn ("Received BYE for a contact not in the switchboard");
		}
		
		[CommandHandler]
		void OnCALReceived (CALCommand cmd)
		{
		}
		
		[CommandHandler]
		void OnACKReceived (ACKCommand cmd)
		{
		}
		
		[ContentHandler]
		void OnClientCapsReceived (ClientCapsContent content)
		{
			Log.Info ("Contact is using {0} ({1})", content.Client, content.Logging ? "Logging" : "Not Logging");
		}
		
		[ContentHandler]
		void OnP2PContentReceived (P2PContent content)
		{
			MsnP2PUtility.ProcessMessage (this, content.P2PMessage);
		}
		
		[ContentHandler]
		void OnKeepAliveContentReceived (KeepAliveContent content)
		{
			// Do nothing, this is just sent to keep the connection alive
		}
		
		[CommandHandler]
		protected override void OnContentReceived (ContentCommand cmd)
		{
			if ((cmd.Content != null) && (!_contentDelegates.ContainsKey (cmd.Content.GetType ())))
			{
				// No handler is registered for this content
				
				MsnConversation conv = (Session.Conversations as MsnConversationManager).GetConversation (this);
				
				if (conv == null)
				{
					if (_contacts.Count == 1)
						conv = (Session.Conversations as MsnConversationManager).GetConversation (_contacts.ToArray ()) as MsnConversation;
					
					if (conv == null)
					{
						// There's also no conversation listening for content from this switchboard
						// We now create one so that it can register content handlers and receive this content
						
						Log.Debug ("Creating conversation for switchboard {0}", _id);
						
						conv = new MsnConversation (this);
						Session.Conversations.Add (conv);
					}
					else
					{
						// There's a conversation with the correct contact which is inactive
						// We can use this instead of creating a new conversation
						
						Log.Debug ("Reactivating conversation {0} for switchboard {1}", conv.ID, _id);
						
						conv.Activate (this);
					}
				}
			}
			
			base.OnContentReceived (cmd);
		}
#pragma warning restore 169
#endregion
		
		public void Invite (MsnContact contact)
		{
			Invite (contact, false);
		}
		
		void Invite (MsnContact contact, bool force)
		{
			_inviteTimeouts[contact] = TimerUtility.RequestCallback (delegate
			{
				if (_inviteTimeouts.ContainsKey (contact))
				{
					TimerUtility.RemoveCallback (_inviteTimeouts[contact]);
					_inviteTimeouts.Remove (contact);
				}
				
				OnInviteContactFailed (new ContactEventArgs (contact));
			}, 30000);
			
			Send (new CALCommand (Session, contact), delegate (IMsnCommand response)
			{
				if (response is CALCommand)
					return; // Invite was ok
				
				// An error occurred
				
				OnInviteContactFailed (new ContactEventArgs (contact));
			}, force);
		}

		protected void OnInviteContactFailed (ContactEventArgs args)
		{
			if (InviteContactFailed != null)
				InviteContactFailed (this, args);
		}
		
		public void Send (P2PMessage msg)
		{
			P2PContent content = new P2PContent (Session);
			content.Data = msg.ToByteArray ();
			
			MSGCommand cmd = content.ToCommand<MSGCommand> ();
			cmd.MIMEHeader["P2P-Dest"] = _contacts[0].UniqueIdentifier;
			
			Send (cmd);
		}
		
		protected override void OnReady (object sender, EventArgs args)
		{
			base.OnReady (sender, args);
			
			if (_contacts.Count == 1)
				OnBridgeReady ();
		}

		protected void OnBridgeReady ()
		{
			if (BridgeReady != null)
				BridgeReady (this, EventArgs.Empty);
		}
		
		protected void OnBridgeClosed ()
		{
			if (BridgeClosed != null)
				BridgeClosed (this, EventArgs.Empty);
		}
		
		protected void OnContactJoined (ContactEventArgs args)
		{
			if (ContactJoined != null)
				ContactJoined (this, args);
		}

		protected void OnContactLeft (ContactEventArgs args)
		{
			if (ContactLeft != null)
				ContactLeft (this, args);
		}
		
		void AuthTimeout ()
		{
			Log.Debug ("Unable to authenticate within 30 seconds");
			Disconnect ();
		}
		
		public override string ToString ()
		{
			string contactStr = string.Empty;
			
			foreach (MsnContact contact in _contacts)
				contactStr += contact.UniqueIdentifier + ", ";
			
			if (contactStr.Length > 2)
				contactStr = contactStr.Substring (0, contactStr.Length - 2);
			
			return string.Format ("SBConnection {0} ({1})", _id, contactStr);
		}
	}
}